/****************************************************************************************
**  File: Imaging.hpp
**  Author: Asteroth
**  Date: 21/03/2009
****************************************************************************************/

#ifndef __IMAGING_HPP__
#define __IMAGING_HPP__

#define __LOADER_PIX__
#define __LOADER_BMP__
//#define __LOADER_PNG__
//#define __LOADER_TGA__

class Pixmap
{
	private:
		estd::Pointer<GLubyte> data;    // Pointer to the image data.
		GLuint width, height, depth;    // Image dimensions.
		GLenum format;

	public:
		Pixmap(GLuint width = 0, GLuint height = 0, GLuint depth = 1, GLenum format = GL_RGB);
		Pixmap(const estd::String &fileName, GLenum allocFormat = 0);

		// Get pixmap attributes.
		inline GLuint getWidth() {return width;}
		inline GLuint getHeight() {return height;}
		inline GLuint getDepth() {return depth;}
		inline GLenum getFormat() {return format;}

		// Get pixels.
		inline estd::Pointer<GLubyte> getData() {return data;}

		// Get bits per pixel.
		inline ubyte bits(GLenum format = 0) const;

		// Load image data from a file, return true if successful, false if file doesn't exist, can't read or format not supported.
		inline bool load(const estd::String &fileName, GLenum allocFormat = 0);

		// Save image data to a file, return true if successful, false if can't create file or format not supported.
		inline bool save(const estd::String &fileName, GLenum saveFormat = 0) const;

		// Reallocates the image with a new size.
		inline bool realloc(GLuint width, GLuint height, GLuint depth = 1, GLenum format = GL_RGB);

		// Frees the data from RAM memory.
		inline bool free();

		// Copies a pixmap.
		inline bool copy(const Pixmap &source);

		// Upload pixmap in video memory, returns the texture handler.
		GLuint createTexture(GLenum wrap = GL_REPEAT, GLenum filter = GL_LINEAR, GLenum format = 0, GLuint depth = 0) const;

		// Convert pixmap's format to another format, the defaultValue will be assigned to missing data (ex. alpha=defaultValue when RGB to RGBA).
		// Return true if successful.
		// Possible conversions:
		//      - RGB, BGR, RGBA, BGRA  ->  RGB, BGR, RGBA, BGRA;
		//      - LUMINANCE_ALPHA, LUMINANCE, ALPHA, RED, GREEN, BLUE  ->  LUMINANCE_ALPHA, LUMINANCE, ALPHA, RED, GREEN, BLUE.
		bool convert(GLenum format, GLubyte defaultValue = 255);

		// Sets a new component value where old component in comparation with a value is true.
		// Possible comparations are: GL_NEVER, GL_ALWAYS, GL_EQUAL, GL_LEQUAL, GL_GEQUAL, GL_LESS, GL_GREATER and GL_NOTEQUAL.
		inline bool set(GLubyte set, GLubyte newValue, GLubyte where, GLubyte oldValue, GLenum cmp = GL_EQUAL);

		// Load a specific file format.
#ifdef __LOADER_PIX__ // Enclave Pixmap
		bool loadPIX(const estd::String &fileName);
		bool savePIX(const estd::String &fileName) const;
#endif
#ifdef __LOADER_BMP__ // Windows Bitmap
		bool loadBMP(const estd::String &fileName);
		bool saveBMP(const estd::String &fileName) const;
#endif
#ifdef __LOADER_PNG__
		bool loadPNG(const estd::String &fileName);
		bool savePNG(const estd::String &fileName) const;
#endif
#ifdef __LOADER_TGA__ // Targa format
		bool loadTGA(const estd::String &fileName);
		bool saveTGA(const estd::String &fileName) const;
#endif
};

Pixmap::Pixmap(GLuint width, GLuint height, GLuint depth, GLenum format)
{
	realloc(width, height, depth, format);
};

Pixmap::Pixmap(const estd::String &fileName, GLenum allocFormat)
{
	load(fileName, allocFormat);
}

ubyte Pixmap::bits(GLenum format) const
{
	if (format==0)
		format = this->format;

    if (format==GL_RGBA || format==GL_BGRA)
        return 32;
    if (format==GL_RGB || format==GL_BGR)
        return 24;
    if (format==GL_LUMINANCE_ALPHA)
        return 16;
    if (format==GL_LUMINANCE || format==GL_ALPHA || format==GL_RED || format==GL_GREEN || format==GL_BLUE)
        return 8;

    return 0;
}

bool Pixmap::load(const estd::String &fileName, GLenum allocFormat)
{
#ifdef __LOADER_PIX__
        if (fileName.getRight(4).getLower() == ".pix")
            return loadPIX(fileName) && (allocFormat==0? true: convert(allocFormat));
#endif
#ifdef __LOADER_BMP__
        if (fileName.getRight(4).getLower() == ".bmp")
            return loadBMP(fileName) && (allocFormat==0? true: convert(allocFormat));
#endif
#ifdef __LOADER_PNG__
        if (fileName.getRight(4).getLower() == ".png")
            return loadPNG(fileName) && (allocFormat==0? true: convert(allocFormat));
#endif
#ifdef __LOADER_TGA__
        if (fileName.getRight(4).getLower() == ".tga")
            return loadTGA(fileName) && (allocFormat==0? true: convert(allocFormat));
#endif

    return false;
}

bool Pixmap::save(const estd::String &fileName, GLenum saveFormat) const
{
	if (saveFormat == 0)
		saveFormat = format;

	Pixmap pxmp;
	bool b = pxmp.copy(*this) && pxmp.convert(saveFormat);

	if (b)
	{
		b = false;

#ifdef __LOADER_PIX__
		if (fileName.getRight(4).getLower() == ".pix")
			b = pxmp.savePIX(fileName);
#endif
#ifdef __LOADER_BMP__
		if (fileName.getRight(4).getLower() == ".bmp")
			b = saveBMP(fileName);
#endif
#ifdef __LOADER_PNG__
		if (fileName.getRight(4).getLower() == ".png")
			b = savePNG(fileName);
#endif
#ifdef __LOADER_TGA__
		if (fileName.getRight(4).getLower() == ".tga")
			b = saveTGA(fileName);
#endif
	}

    return b;
}

bool Pixmap::realloc(GLuint width, GLuint height, GLuint depth, GLenum format)
{
	ubyte bpp = bits(format);

	if (width>0 && height>0 && depth>0 && bpp>0)
	{
		int bc = width * height * depth * bpp;

		if (data.realloc(bc))
		{
			this->format = format;
			this->width = width;
			this->height = height;
			this->depth = depth;

			return true;
		}
		else
			return false;
	}

	this->format = GL_RGB;
	this->data = NULL;
	this->width = this->height = this->depth = 0;
	this->format = format;

	return true;
}

bool Pixmap::free()
{
    if (data.isNull())
        return false;
    data = NULL;
    width = height = depth = 0;
    format = GL_RGB;
    return true;
}

bool Pixmap::copy(const Pixmap &source)
{
    if (!source.data.isNull())
    {
        width = source.width;
        height = source.height;
        depth = source.depth;
        format = source.format;

        GLubyte bpp = source.bits()/8;
        GLuint bc = width*height*depth*bpp;
        data.realloc(bc);
        eArrayCopy(GLubyte, &data[0], &source.data[0], bc);

        return true;
    }

    return false;
}

GLuint Pixmap::createTexture(GLenum wrap, GLenum filter, GLenum format, GLuint depth) const
{
    if (data.isNull())
        return 0;

    if (depth>=this->depth)
        return 0;

    if (format==0)
        format = this->format;

    if (format==GL_BGR)
		format = GL_RGB;
    else if (format==GL_BGRA)
    	format = GL_RGBA;

    bool texEn = glIsEnabled(GL_TEXTURE_2D);
    if ( !texEn )
        glEnable(GL_TEXTURE_2D);

    GLuint texID;
    glGenTextures(1, &texID);
    if (texID==0)
    	return 0;

    glBindTexture(GL_TEXTURE_2D, texID);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    //Set texture parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

    //Upload the texture
    glTexImage2D(GL_TEXTURE_2D,
        0,                  //level of detail
        format,             //internal format
        this->width,       //image width
        this->height,      //image height
        0,                  //use borders?
        this->format,      //texture format
        GL_UNSIGNED_BYTE,   //data type
        &(data[this->width * this->height * depth])
        );

    if ( !texEn )
        glDisable(GL_TEXTURE_2D);

    return texID;
}

bool Pixmap::convert(GLenum format, GLubyte defaultValue)
{
    if (this->format==format)
        return true;

    // Inverts color components order.
    // RGBA > BGRA
    // BGRA > RGBA
    // RGB  > BGR
    // BGR  > RGB
    if ((this->format==GL_RGBA && format==GL_BGRA) || (this->format==GL_BGRA && format==GL_RGBA)
        || (this->format==GL_RGB && format==GL_BGR) || (this->format==GL_BGR && format==GL_RGB))
    {
        GLubyte bpp = bits()/8;
        GLuint bc = width * height * depth * bpp;
        GLubyte tmp;

        for (GLuint i=0; i<bc; i+=bpp)
        {
            tmp = data[i];
            data[i] = data[i+2];
            data[i+2] = tmp;
        }

        this->format = format;
        return true;
    }
    // Adds alpha component and inverts color components order if needed.
    // RGB  > RGBA
    // RGB  > BGRA
    // BGR  > RGBA
    // BGR  > BGRA
    else if ((this->format==GL_RGB && (format==GL_RGBA || format==GL_BGRA))
        || (this->format==GL_BGR && (format==GL_RGBA || format==GL_BGRA)))
    {
        GLuint pc = width * height * depth * 3;
        GLuint nc = width * height * depth * 4;

        estd::Pointer<GLubyte> tmpData = data;
        data.realloc(nc);

        if ((this->format==GL_RGB && format==GL_RGBA) || (this->format==GL_BGR && format==GL_BGRA))
        {
        	for (GLuint i=0, j=0; i<pc || j<nc; i+=3, j+=4)
			{
				data[j  ] = tmpData[i    ];
				data[j+1] = tmpData[i + 1];
				data[j+2] = tmpData[i + 2];
				data[j+3] = defaultValue;
			}
        }
        else
        {
        	for (GLuint i=0, j=0; i<pc || j<nc; i+=3, j+=4)
			{
				data[j  ] = tmpData[i + 2];
				data[j+1] = tmpData[i + 1];
				data[j+2] = tmpData[i    ];
				data[j+3] = defaultValue;
			}
        }

        tmpData = NULL;
        this->format = format;
        return true;
    }
    // Deletes alpha component and inverts color components order if needed.
    // RGBA > RGB
    // RGBA > BGR
    // BGRA > RGB
    // BGRA > BGR
    else if ((this->format==GL_RGBA && (format==GL_RGB || format==GL_BGR))
        || (this->format==GL_BGRA && (format==GL_RGB || format==GL_BGR)))
    {
        GLuint pc = width * height * depth * 4;
        GLuint nc = width * height * depth * 3;

        estd::Pointer<GLubyte> tmpData = data;
        data.realloc(nc);

        if ((this->format==GL_RGBA && format==GL_RGB) || (this->format==GL_BGRA && format==GL_BGR))
        {
        	for (GLuint i=0, j=0; i<pc || j<nc; i+=4, j+=3)
			{
				data[j  ] = tmpData[i    ];
				data[j+1] = tmpData[i + 1];
				data[j+2] = tmpData[i + 2];
			}
        }
        else
        {
        	for (GLuint i=0, j=0; i<pc || j<nc; i+=4, j+=3)
			{
				data[j  ] = tmpData[i + 2];
				data[j+1] = tmpData[i + 1];
				data[j+2] = tmpData[i    ];
			}
        }

        tmpData = NULL;
        this->format = format;
        return true;
    }
    else if ((this->format==GL_LUMINANCE || this->format==GL_ALPHA || this->format==GL_RED || this->format==GL_GREEN || this->format==GL_BLUE)
        && (format==GL_LUMINANCE || format==GL_ALPHA || format==GL_RED || format==GL_GREEN || format==GL_BLUE))
    {
    	this->format = format;
        return true;
    }
    else if ((this->format==GL_LUMINANCE || this->format==GL_ALPHA || this->format==GL_RED || this->format==GL_GREEN || this->format==GL_BLUE)
        && format==GL_LUMINANCE_ALPHA)
    {
        GLuint pc = width * height * depth * 1;
        GLuint nc = width * height * depth * 2;

        estd::Pointer<GLubyte> tmpData = data;
        data.realloc(nc);

        for (GLuint i=0, j=0; i<pc || j<nc; i+=1, j+=2)
        {
            data[j  ] = tmpData[i];
            data[j+1] = defaultValue;
        }

        tmpData = NULL;
        this->format = format;
        return true;
    }
    else if (this->format==GL_LUMINANCE_ALPHA
        && (format==GL_LUMINANCE || format==GL_ALPHA || format==GL_RED || format==GL_GREEN || format==GL_BLUE))
    {
        GLuint pc = width * height * depth * 2;
        GLuint nc = width * height * depth * 1;

        estd::Pointer<GLubyte> tmpData = data;
        data.realloc(nc);

        for (GLuint i=0, j=0; i<pc || j<nc; i+=2, j+=1)
            data[j] = tmpData[i];

        tmpData = NULL;
        this->format = format;
        return true;
    }

    return false;
}

bool Pixmap::set(GLubyte set, GLubyte newValue, GLubyte where, GLubyte oldValue, GLenum cmp)
{
	int bpp = bits() / 8;
	int pc = width * height * depth * bpp;

	if (bpp==0 || pc==0 || set>=bpp || where>=bpp)
		return false;

	switch (cmp)
	{
		case GL_NEVER:
			return true;
		case GL_ALWAYS:
			cout <<"BPP: " <<bpp <<", PC: " <<pc <<".\n";
			for (int i=0; i<pc; i+=bpp)
				data[i+set] = newValue;
			return true;
		case GL_EQUAL:
			for (int i=0; i<pc; i+=bpp)
				if (data[i+where]==oldValue)
					data[i+set] = newValue;
			return true;
		case GL_LEQUAL:
			for (int i=0; i<pc; i+=bpp)
				if (data[i+where]<=oldValue)
					data[i+set] = newValue;
			return true;
		case GL_GEQUAL:
			for (int i=0; i<pc; i+=bpp)
				if (data[i+where]>=oldValue)
					data[i+set] = newValue;
			return true;
		case GL_LESS:
			for (int i=0; i<pc; i+=bpp)
				if (data[i+where]<oldValue)
					data[i+set] = newValue;
			return true;
		case GL_GREATER:
			for (int i=0; i<pc; i+=bpp)
				if (data[i+where]>oldValue)
					data[i+set] = newValue;
			return true;
		case GL_NOTEQUAL:
			for (int i=0; i<pc; i+=bpp)
				if (data[i+where]!=oldValue)
					data[i+set] = newValue;
			return true;
	}
	return false;
}

// Helper functions
#ifdef __LOADER_PIX__
#define __PIX_MAGIC_NUMBER__ 0x5A83FC02
// --- EPIX format -----------------
// GLuint magicNumber;
// GLuint imageWidth;
// GLuint imageHeight;
// GLuint imageDepth;
// GLenum imageFormat;
// GLubyte imageData[imageWidth*imageHeight*imageDepth*bytesPerPixel]; // bytesPerPixel is calculated this way: eglPixmapBits(pixmap)/8;
// ---------------------------------
bool Pixmap::loadPIX(const estd::String &fileName)
{
    GLuint tmp4;

    FILE* file = fopen(fileName, "rb");
    if(file==NULL)
    {
        data = NULL;
        width = height = depth = 0;
        format = GL_RGB;
        return false;
    }

    fread((char*)&tmp4, 4, 1, file);
    if (tmp4!=__PIX_MAGIC_NUMBER__)
    {
        fclose(file);
        data = NULL;
        width = height = depth = 0;
        format = GL_RGB;
        return false;
    }

    fread((char*)&width, 4, 1, file);
    fread((char*)&height, 4, 1, file);
    fread((char*)&depth, 4, 1, file);
    fread((char*)&format, 4, 1, file);

    GLubyte bpp = bits()/8;
    GLuint bc = width * height * depth * bpp;

    data.realloc(bc);
    if (fread((char*)&data[0], bc, 1, file)!=1)
    {
        fclose(file);
        data = NULL;
        width = height = depth = 0;
        format = GL_RGB;
        return false;
    }

    return true;
}
bool Pixmap::savePIX(const estd::String &fileName) const
{
    if (data.isNull())
        return false;

    GLubyte bpp = bits()/8;
    GLuint bc = width * height * depth * bpp;

    FILE* file = fopen(fileName, "wb");
    if(file==NULL)
        return false;

    GLuint tmp4 = __PIX_MAGIC_NUMBER__;

    fwrite((char*)&tmp4, 4, 1, file);
    fwrite((char*)&width, 4, 1, file);
    fwrite((char*)&height, 4, 1, file);
    fwrite((char*)&depth, 4, 1, file);
    fwrite((char*)&format, 4, 1, file);

    if (fwrite((char*)&data[0], bc, 1, file)!=1)
    {
        fclose(file);
        return false;
    }

    fclose(file);
    return true;
}
#endif

#ifdef __LOADER_BMP__
bool Pixmap::loadBMP(const estd::String &fileName)
{
    GLushort    tmp2;
    GLuint      tmp4;
    char tmp64[64];

    FILE* file = fopen(fileName, "rb");
    if(file==NULL)
    {
        data = NULL;
        width = height = depth = 0;
        format = GL_RGB;
        return false;
    }

    // File header
    fread(tmp64, 14, 1, file);
    // Bmp header
    fread((char*)&tmp4, 4, 1, file);
    fread((char*)&tmp4, 4, 1, file);
    width = tmp4;
    fread((char*)&tmp4, 4, 1, file);
    height = tmp4;
    fread((char*)&tmp2, 2, 1, file);
    depth = tmp2;
    GLushort bits;
    fread((char*)&bits, 2, 1, file);
    fread(tmp64, 24, 1, file);

    GLubyte bpp = bits/8;
    GLuint bc = width * height * depth * bpp;

    if (bpp==4)
        format = GL_BGRA;
    else if (bpp==3)
        format = GL_BGR;
    else if (bpp==2)
        format = GL_LUMINANCE_ALPHA;
    else if (bpp==1)
        format = GL_ALPHA;
    else
    {
        data = NULL;
        width = height = depth = 0;
        format = GL_RGB;
        fclose(file);
        return false;
    }

    if (data.realloc(bc))
    {
    	if (fread((char*)&data[0], bc, 1, file)==1)
    	{
    		fclose(file);
    		return true;
    	}
    }

    data = NULL;
	width = height = depth = 0;
	format = GL_RGB;
	fclose(file);
	return false;
}
bool Pixmap::saveBMP(const estd::String &fileName) const
{
    if (data.isNull())
        return false;

    GLubyte bpp = bits()/8;
    GLuint bc = width * height * depth * bpp;

    GLushort    fType = 19778;
    GLuint      fSize = 54 + bc;
    GLushort    fReserved1 = 0;
    GLushort    fReserved2 = 0;
    GLuint      fOffsetBytes = 54;

    GLuint      hSize = 40;
    GLuint      hWidth = width;
    GLuint      hHeight = height;
    GLushort    hPlanes = depth;
    GLushort    hBitCount = bpp * 8;
    GLuint      hCompression = 0;
    GLuint      hSizeImage = bc;
    GLint       hXpelspermeter = 3780;
    GLint       hYpelspermeter = 3780;
    GLuint      hColorsUsed = 0;
    GLuint      hColorsImportant = 0;

    FILE* file = fopen(fileName, "wb");
    if(file==NULL)
        return false;

    fwrite((char*)&fType, 2, 1, file);
    fwrite((char*)&fSize, 4, 1, file);
    fwrite((char*)&fReserved1, 2, 1, file);
    fwrite((char*)&fReserved2, 2, 1, file);
    fwrite((char*)&fOffsetBytes, 4, 1, file);

    fwrite((char*)&hSize, 4, 1, file);
    fwrite((char*)&hWidth, 4, 1, file);
    fwrite((char*)&hHeight, 4, 1, file);
    fwrite((char*)&hPlanes, 2, 1, file);
    fwrite((char*)&hBitCount, 2, 1, file);
    fwrite((char*)&hCompression, 4, 1, file);
    fwrite((char*)&hSizeImage, 4, 1, file);
    fwrite((char*)&hXpelspermeter, 4, 1, file);
    fwrite((char*)&hYpelspermeter, 4, 1, file);
    fwrite((char*)&hColorsUsed, 4, 1, file);
    fwrite((char*)&hColorsImportant, 4, 1, file);

    if (fwrite((char*)&data[0], bc, 1, file)!=1)
    {
        fclose(file);
        return false;
    }

    fclose(file);
    return true;
}
#endif

#ifdef __LOADER_PNG__
bool Pixmap::loadPNG(const estd::String fileName)
{
    return false;
}
bool Pixmap::savePNG(const estd::String fileName)
{
    return false;
}
#endif

#ifdef __LOADER_TGA__
bool Pixmap::loadTGA(const estd::String fileName)
{
    return false;
}
bool Pixmap::saveTGA(const estd::String fileName)
{
    return false;
}
#endif

#endif
